<?php

declare(strict_types=1);

namespace app\libs\backend;

use app\BaseController;
use think\Validate;
use app\validate\BaseValidate;
use app\middleware\{AuthComment};
use think\facade\Db;
use think\facade\Event;

/**
 * 后台基础控制器
 * 
 * ---- 配置项 ----
 * 参考本类中所有protected属性
 * 
 * ---- 方法 ----
 * 自带了如下方法，如需要可以覆写。如果要重载api文档，可以覆写同时返回本类的方法即可，例如 return parent::list();
 * list() 		获取列表
 * edit() 		新增/编辑
 * delete() 	删除
 * 
 * ---- 事件 ----
 * 内置了一些事件，可以在控制器中注册监听器，例如：
	Event::listen('CrudBeforeEdit-' . {modelClass}, function ($data) {
		$params = $data['params'];
		if($params['age'] > 10){
			// do something...
		}
		return $params;
	});

 * 事件有如下：
 * list查询初始化 		'CrudInitList-{modelClass}' 				() 													需要返回$query实例
 * list查询前 			'CrudBeforeList-{modelClass}' 				(['query']) 										需要返回$query实例
 * list查询后 			'CrudAfterList-{modelClass}' 				(['total','rows']) 									需要返回$query实例
 * edit[编辑/新增]前 	'CrudBefore[Edit/Create]-{modelClass}' 		(['params', 'model', 'pk(表示是否编辑)']) 			需要返回$params
 * edit写入前 			'CrudBeforeSave-{modelClass}' 				(['params', 'model', 'pk(表示是否编辑)']) 			需要返回$params
 * edit写入后 			'CrudAfterSave-{modelClass}' 				(['params', 'model', 'pk(表示是否编辑)']) 			无返回
 * edit[编辑/新增]后 	'CrudAfter[Edit/Create]-{modelClass}' 		(['params', 'model', 'pk(表示是否编辑)']) 			无返回
 * delete删除前 		'CrudBeforeDelete-{modelClass}' 			(['ids(数组)']) 									无返回
 * delete删除后 		'CrudAfterDelete-{modelClass}' 				(['ids(数组)', 'delete(删除结果)']) 				无返回
 * 
 */
class CrudController extends BaseController
{

	//中间件
	protected $middleware = [AuthComment::class];

	/**
	 * 对应的模型实例
	 * @var string|null
	 */
	protected $modelClass;

	/**
	 * list接口允许search_word搜索的字段
	 * 可以是字符串用|分割，或者是一个回调函数，如果是false则关闭搜索功能
	 * @var false|string|callable
	 */
	protected $listSearchFields = 'name|id';

	/**
	 * list接口的$hidden功能
	 * 默认false表示使用model中声明的$hidden，true表示禁用$hidden，array表示覆盖$hidden
	 * @var boolean|array
	 */
	protected $listModelHidden = false;

	/**
	 * edit接口限定允许的字段
	 * 如果设置本字段，则不自动获取所有字段，而是采用本设置（排除字段依然生效）
	 * @var null|array
	 */
	protected $editAllowFields = null;

	/**
	 * edit接口排除的字段
	 * @var null|array
	 */
	protected $editExceptFields = ['id', 'create_time', 'update_time', 'delete_time'];

	/**
	 * edit接口验证器
	 * 是null自动获取\app\validate\BaseValidate，也可以是类名或类实例，如果设为false则关闭验证
	 * @var null|string|Validate|false
	 */
	protected $editValidate = null;

	/**
	 * edit接口验证规则（合并）
	 * 每一项作为一个field，值是一个字符串或字符串数组
	 * 例如： ['user_id' => ['require','integer','checkRecord:User']]
	 * 例如： ['user_id' => 'max:4|>=:2']
	 * @var array
	 */
	protected $editValidateMergeRule = [];

	/**
	 * edit接口自定义验证规则
	 * 为null则自动获取，若为数组则完全自定义，会原样传给 validate()->check($params, $this->editValidateRule);
	 * @var null|array
	 */
	protected $editValidateRule = null;

	/**
	 * edit接口是否批量验证
	 * @var bool
	 */
	protected $batchValidate = true;

	/**
	 * 获取数据库字段信息对应的中文注释名
	 * 会移除其中的()等内容
	 * @param array $fieldInfo 数据库字段类型信息（name,type,notnull,...）
	 */
	protected function ccFieldCommentName($fieldInfo)
	{
		return preg_replace('/\(.+?\)|（.+?）/', '', $fieldInfo['comment']);
	}

	/**
	 * 获取数据库字段类型对应的默认验证规则
	 * @param array $fieldInfo 数据库字段类型信息（name,type,notnull,...）
	 */
	protected function ccFieldTypeValidateRule($fieldInfo)
	{
		$rules = [];
		// 判断是否require
		if ($fieldInfo['notnull'] && is_null($fieldInfo['default'])) {
			$rules[] = 'require';
		}
		// 判断类型
		if (preg_match('/^(int|tinyint|bigint)/', $fieldInfo['type'])) {
			$rules[] = 'integer';
		} else if (preg_match('/^(varchar)/', $fieldInfo['type'])) {
			$rules[] = 'max:255';
		}
		// 判断关联，这里的正则逻辑是 任意字符串以 _id 结尾
		if (preg_match('/(_id)/', $fieldInfo['name'])) {
			// 但这也不一定有关联，要看对应模型类是否存在
			$modelName = parse_name(substr($fieldInfo['name'], 0, -3), 1);
			if (class_exists('app\model\\' . $modelName)) {
				$rules[] = 'checkRecord:' . $modelName;
			}
		}
		return $rules;
	}

	/**
	 * @api list
	 * @param string $search_word
	 */
	public function list()
	{
		$eventResult = Event::trigger('CrudInitList-' . $this->modelClass, null, true);
		$query = $eventResult
			? $eventResult
			: $this->modelClass::scope('FilterFields', [])->scope('WithAppends', [])->scope('FilterTime', []);

		// 搜索词功能
		if ($this->listSearchFields !== false) {
			$search_word = input('search_word');
			$query->when($search_word, function ($query) use ($search_word) {
				if (is_callable($this->listSearchFields)) {
					($this->listSearchFields)($query, $search_word);
				} else {
					$query->where($this->listSearchFields, 'like', '%' . $search_word . '%');
				}
			});
		}

		// 事件处理
		$eventResult = Event::trigger('CrudBeforeList-' . $this->modelClass, ['query' => $query], true);
		if ($eventResult !== false) $query = $eventResult;

		$total = $query->count();
		$rows = $query->scope('Pagesort')->select();

		// 是否禁用hidden
		if ($this->listModelHidden) {
			$rows = $rows->hidden(is_array($this->listModelHidden) ? $this->listModelHidden : []);
		}

		$result = ['total' => $total, 'rows' => $rows];

		// 事件处理
		$eventResult = Event::trigger('CrudAfterList-' . $this->modelClass, ['result' => $result, 'query' => $query], true);
		if ($eventResult !== false)  $result = $eventResult;

		return success($result);
	}

	/**
	 * @api add/edit
	 */
	public function edit()
	{
		$modelInstance = new $this->modelClass;
		$pkField = $modelInstance->getPk();
		$pk = request()->param($pkField);

		$item = $pk ? $modelInstance->findOrFail($pk) : $modelInstance;

		$fieldsInfo = $modelInstance->getFields();

		$allowFields = array_diff(
			is_array($this->editAllowFields) ? $this->editAllowFields : array_keys($fieldsInfo),
			array_merge([$pkField], $this->editExceptFields ?? [])
		);

		$params = request()->only($allowFields);
		if (empty($params)) return failed('未收到任何参数');

		// 验证
		if ($this->editValidate !== false) {
			/** @var Validate */
			$validate = ($this->editValidate instanceof Validate) ? $this->editValidate : (
				is_string($this->editValidate) ? new $this->editValidate : new BaseValidate()
			);

			// 处理规则
			$rules = $this->editValidateRule;
			if (is_null($rules)) {
				$rules = [];
				foreach ($allowFields as $fieldName) {
					$fieldInfo = $fieldsInfo[$fieldName];
					$rule = $this->ccFieldTypeValidateRule($fieldInfo); // 自动计算的规则数组
					// 把下面的foreach用一行代码写出来
					if (isset($this->editValidateMergeRule[$fieldName])) {
						$rule = array_merge(
							$rule,
							is_string($this->editValidateMergeRule[$fieldName])
								? explode('|', $this->editValidateMergeRule[$fieldName])
								: $this->editValidateMergeRule[$fieldName]
						);
					}
					if (count($rule)) {
						$rules[$fieldName . '|' . $this->ccFieldCommentName($fieldInfo)] = implode('|', $rule);
					}
				}
			}

			$validate->failException(true)->batch($this->batchValidate)->check($params, $rules);
		}

		// 事件处理
		// Event::trigger 最后的$once传true则只会触发第一个事件监听器，并获得第一个返回值，所以这样写的话，设置多个监听器后面的是不生效的
		if ($pk) {
			$eventResult = Event::trigger('CrudBeforeEdit-' . $this->modelClass, ['model' => $item, 'params' => $params, 'pk' => $pk], true);
		} else {
			$eventResult = Event::trigger('CrudBeforeCreate-' . $this->modelClass, ['model' => $item, 'params' => $params, 'pk' => $pk], true);
		}
		if ($eventResult !== false) $params = $eventResult;
		$eventResult = Event::trigger('CrudBeforeSave-' . $this->modelClass, ['model' => $item, 'params' => $params, 'pk' => $pk], true);
		if ($eventResult !== false) $params = $eventResult;

		$item->save($params);

		// 事件处理
		Event::trigger('CrudAfterSave-' . $this->modelClass, ['model' => $item, 'params' => $params, 'pk' => $pk]);
		if ($pk) {
			Event::trigger('CrudAfterEdit-' . $this->modelClass, ['model' => $item, 'params' => $params, 'pk' => $pk]);
		} else {
			Event::trigger('CrudAfterCreate-' . $this->modelClass, ['model' => $item, 'params' => $params, 'pk' => $pk]);
		}

		return success($item);
	}

	/**
	 * @api delete
	 * @param number|array $id on,or,array
	 */
	public function delete()
	{
		$id = input('id');
		$ids = is_array($id) ? $id : [$id];

		Event::trigger('CrudBeforeDelete-' . $this->modelClass, ['ids' => $ids]);
		Db::startTrans();
		$delete = $this->modelClass::destroy($ids);
		Db::commit();
		Event::trigger('CrudAfterDelete-' . $this->modelClass, ['ids' => $ids, 'delete' => $delete]);

		return success($delete);
	}
}
